/*
 Copyright (c) 2008  Seth Bourget  <seth@cafesilencio.net>
 All rights reserved.

 Licensed under the Apache License, Version 2.0 (the "License"); 
 you may not use this file except in compliance with the License. 
 You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 
 Unless required by applicable law or agreed to in writing, 
 software distributed under the License is distributed on an "AS IS" BASIS, 
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 See the License for the specific language governing permissions and limitations under the License. 

 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
 PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

version 1.0 - Initial Release

version 1.1
added support for SQL syntax queries
NOTE: the QueryWithAttributes method is being deprecated by Amazon.com
http://developer.amazonwebservices.com/connect/ann.jspa?annID=409
*/

package net.cafesilencio.awssimpledb
{
	import com.hurlant.crypto.hash.HMAC;
	import com.hurlant.crypto.hash.SHA1;
	import com.hurlant.util.Base64;
	
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IOErrorEvent;
	import flash.net.URLLoader;
	import flash.net.URLRequest;
	import flash.net.URLVariables;
	import flash.utils.ByteArray;
	
	import mx.formatters.DateFormatter;

	
	public class AWSSimpleDB extends EventDispatcher
	{
		private static var _awsAccessKey:String = "";
		private static var _awsSecretKey:String = "";
		private static var remoteRequestURL:String = "";
		private static var endPointURLExtendsion:String = "/";

public  static const AMAZON_SIMPLEDB_ENDPOINT:String = null;//not used		
		
		private static const PROTOCOL_HTTPS:String = "https://";
		private static const PROTOCOL_HTTP:String = "http://";
		private static var protocol:String = PROTOCOL_HTTP;
		
		
		private static var domainEndpoint:String = DOMAIN_ENDPOINT_AMAZON;
		
		private static const mDBURLExtension:String = "/mdb/request.mgwsi";
		private static const DOMAIN_ENDPOINT_AMAZON:String 			= "sdb.amazonaws.com";
		private static const ACTION_CREATE_DOMAIN:String 			= "CreateDomain";
		private static const ACTION_DELETE_DOMAIN:String 			= "DeleteDomain";
		private static const ACTION_LIST_DOMAINS:String	 			= "ListDomains";
		private static const ACTION_PUT_ATTRIBUTES:String 			= "PutAttributes";
		private static const ACTION_GET_ATTRIBUTES:String 			= "GetAttributes";
		private static const ACTION_DELETE_ATTRIBUTES:String 		= "DeleteAttributes";
		private static const ACTION_QUERY:String 					= "Query";
		private static const ACTION_QUERY_WITH_ATTRIBUTES:String 	= "QueryWithAttributes";
		private static const ACTION_GET_DOMAIN_METADATA:String		= "DomainMetadata";
		private static const ACTION_QUERY_WITH_SQL_SYNTAX:String	= "Select";
		
		public static const SIGNATURE_VERSION_0:int = 0;
		public static const SIGNATURE_VERSION_1:int = 1;//not implemented
		public static const SIGNATURE_VERSION_2:int = 2;
		
		private static var signatureVersionToUse:int = SIGNATURE_VERSION_2;
		
		private var dateFormatter:DateFormatter;
		private var sdbqo:SimpleDBQueryObject = null;//noOp
		
		
		
		/*********************************************************************/
		
		public function AWSSimpleDB()
		{
			dateFormatter = new DateFormatter();		
			dateFormatter.formatString = "YYYY-MM-DTJ:NN:SSZ";
			
			remoteRequestURL = protocol + domainEndpoint + endPointURLExtendsion;
		}
		
		/*********************************************************************/
		
		public function useMDB(p0:Boolean,endpointIPAddress:String):void
		{
			if(p0)
			{
				domainEndpoint = endpointIPAddress;
				endPointURLExtendsion = mDBURLExtension;
				protocol = PROTOCOL_HTTP;
			}
			else
			{
				domainEndpoint = DOMAIN_ENDPOINT_AMAZON;
				protocol = PROTOCOL_HTTP;
				endPointURLExtendsion = "/";
			}
			remoteRequestURL = protocol + domainEndpoint + endPointURLExtendsion;
		}
		
		/*********************************************************************/
		
		public function useSSL(p0:Boolean):void
		{
			if(p0)
				protocol = PROTOCOL_HTTPS;
			else
				protocol = PROTOCOL_HTTP;
		}
		
		/*********************************************************************/
		
		public function setSignatureVersion(sigVer:int):void
		{
			signatureVersionToUse = sigVer;
		}

		/*********************************************************************/

		public function setAWSCredentials(accessKey:String,secretKey:String):void
		{
			_awsAccessKey = accessKey;
			_awsSecretKey = secretKey;
		}
		
		/*********************************************************************/
		
		public function getS3AccessKey():String
		{	return _awsAccessKey;	}
		
		/*********************************************************************/
		
		public function getS3SecretKey():String
		{	return _awsSecretKey;	}
		
		
		/*********************************************************************/
		
		public function getDomainMetaData(domainName:String):void
		{
			var urlVarsArr:Array = new Array();
				urlVarsArr.push(new SimpleDBAttributeObject("DomainName",domainName));
				
			executeRequest(ACTION_GET_DOMAIN_METADATA,urlVarsArr,"POST");
		}
		
		/*********************************************************************/
		
		public function createDomain(domainName:String):void
		{
			var urlVarsArr:Array = new Array();
				urlVarsArr.push(new SimpleDBAttributeObject("DomainName",domainName));			

			executeRequest(ACTION_CREATE_DOMAIN,urlVarsArr,"POST");
		}
		
		/*********************************************************************/
		
		public function deleteDomain(domainName:String):void
		{
			var urlVarsArr:Array = new Array();
				urlVarsArr.push(new SimpleDBAttributeObject("DomainName",domainName));			
			
			executeRequest(ACTION_DELETE_DOMAIN,urlVarsArr,"POST");		
		}
		
		/*********************************************************************/
		
		public function listAllDomains(nextToken:String = null):void
		{		
			var urlVarsArr:Array = new Array();
			if(nextToken != null && nextToken != "")
				urlVarsArr.push(new SimpleDBAttributeObject("NextToken",nextToken));
			
			executeRequest(ACTION_LIST_DOMAINS,urlVarsArr,"POST");		
		}
		
		
		/*********************************************************************/
		/*
		*	@attributes should be array of SimpleDBAttributeObjects
		*/		
		//SBNOTE: should come back later and allow for granular replace control for each attribute.
		public function putAttributes(domainName:String,itemName:String, attributes:Array, replace:Boolean = true):void
		{
			var urlVarsArr:Array = new Array();
				urlVarsArr.push(new SimpleDBAttributeObject("DomainName",domainName));
				urlVarsArr.push(new SimpleDBAttributeObject("ItemName",itemName));			
			
			for(var i:int = 0; i < attributes.length; i++)
			{
				if(attributes[i] is SimpleDBAttributeObject)
				{
					var att:SimpleDBAttributeObject = attributes[i] as SimpleDBAttributeObject;
					urlVarsArr.push(new SimpleDBAttributeObject("Attribute." + i + ".Name",att.getAttributeName()));
					urlVarsArr.push(new SimpleDBAttributeObject("Attribute." + i + ".Value",att.getAttributeValue()));
					urlVarsArr.push(new SimpleDBAttributeObject("Attribute." + i + ".Replace",replace.toString()));
				}
			}
			executeRequest(ACTION_PUT_ATTRIBUTES,urlVarsArr,"POST");
		}
		
		/*********************************************************************/
		/*
		*	@attributes should be array of SimpleDBAttributeObjects
		*/
		//A null value for the @attributesToGet variable will get all attributes for the item.
		public function getAttributes(domainName:String,itemName:String,attributesToGet:Array = null):void
		{
			var urlVarsArr:Array = new Array();
				urlVarsArr.push(new SimpleDBAttributeObject("DomainName",domainName));
				urlVarsArr.push(new SimpleDBAttributeObject("ItemName",itemName));			
				
			if(attributesToGet != null)
			{
				for(var i:int = 0; i < attributesToGet.length; i++)
					urlVarsArr.push(new SimpleDBAttributeObject("AttributeName." + i,attributesToGet[i]));		
			}
			executeRequest(ACTION_GET_ATTRIBUTES,urlVarsArr,"POST");
		}
		
		/*********************************************************************/
		/*
		*	@attributes should be array of SimpleDBAttributeObjects
		*/
		public function removeAttributes(domainName:String,itemName:String,attributes:Array = null):void
		{
			var urlVarsArr:Array = new Array();
				urlVarsArr.push(new SimpleDBAttributeObject("DomainName",domainName));
				urlVarsArr.push(new SimpleDBAttributeObject("ItemName",itemName));					
				
			if(attributes != null)
			{
				for(var i:int = 0; i < attributes.length; i++)
				{
					if(attributes[i] is SimpleDBAttributeObject)
					{
						var att:SimpleDBAttributeObject = attributes[i] as SimpleDBAttributeObject;
						urlVarsArr.push(new SimpleDBAttributeObject("Attribute." + i + ".Name",att.getAttributeName()));
						if(att.getAttributeValue() != null && att.getAttributeValue() != "")
							urlVarsArr.push(new SimpleDBAttributeObject("Attribute." + i + ".Value",att.getAttributeValue()));
					}
				}
			}
			executeRequest(ACTION_DELETE_ATTRIBUTES,urlVarsArr,"POST");
		}
		
		/*********************************************************************/
		
		public function queryForItems(domainName:String,maxItemsToReturn:int = 250,nextToken:String = null,queryExpression:String = null):void
		{
			var urlVarsArr:Array = new Array();
				urlVarsArr.push(new SimpleDBAttributeObject("DomainName",domainName));			
				urlVarsArr.push(new SimpleDBAttributeObject("MaxNumberOfItems",maxItemsToReturn.toString()));
				
			if(nextToken != null && nextToken != "")
				urlVarsArr.push(new SimpleDBAttributeObject("NextToken",nextToken));
			
			if(queryExpression != null && queryExpression != "")
				urlVarsArr.push(new SimpleDBAttributeObject("QueryExpression",queryExpression));
				
			executeRequest(ACTION_QUERY,urlVarsArr,"POST");
		}
		
		
		/*********************************************************************/
		//@AttributeName = The name of the attribute to return. To return multiple attributes, 
		//you can specify this request parameter multiple times. 
		//Default: None. If this request parameter is not specified or left blank, all attributes are returned. 
		public function queryForAttributes(domainName:String,attributeNames:Array = null,maxItemsToReturn:int = 250,nextToken:String = null,queryExpression:String = null):void
		{
			var urlVarsArr:Array = new Array();
				urlVarsArr.push(new SimpleDBAttributeObject("DomainName",domainName));			
				urlVarsArr.push(new SimpleDBAttributeObject("MaxNumberOfItems",maxItemsToReturn.toString()));			
				
			if(attributeNames != null)
			{
				for(var i:int = 0; i < attributeNames.length; i++)
					urlVarsArr.push(new SimpleDBAttributeObject("AttributeName." + (i+1).toString(),attributeNames[i]));
			}
				
			if(nextToken != null && nextToken != "")
				urlVarsArr.push(new SimpleDBAttributeObject("NextToken",nextToken));
			
			if(queryExpression != null && queryExpression != "")
				urlVarsArr.push(new SimpleDBAttributeObject("QueryExpression",queryExpression));
				
			executeRequest(ACTION_QUERY_WITH_ATTRIBUTES,urlVarsArr,"POST");					
		}
		
		/*********************************************************************/
		
		public function queryUsingSQLSyntax(selectExpression:String,maxItemsToReturn:int = 250,nextToken:String = null):void
		{
			var urlVarsArr:Array = new Array();			
				urlVarsArr.push(new SimpleDBAttributeObject("MaxNumberOfItems",maxItemsToReturn.toString()));
				
			if(nextToken != null && nextToken != "")
				urlVarsArr.push(new SimpleDBAttributeObject("NextToken",nextToken));
				
			urlVarsArr.push(new SimpleDBAttributeObject("SelectExpression",selectExpression));	
			
			executeRequest(ACTION_QUERY_WITH_SQL_SYNTAX,urlVarsArr,"POST");									
		}
		
		/*********************************************************************/
		
		private function executeRequest(action:String,urlVariablesArr:Array,requestMethod:String):void
		{
			if(_awsAccessKey == null || _awsAccessKey == "" || _awsSecretKey == null || _awsSecretKey == "")
			{
				dispatchEvent(new SimpleDBEvent(SimpleDBEvent.SIMPLEDB_CREDENTIALS_NOT_SET));
				return;
			}

			urlVariablesArr.push(new SimpleDBAttributeObject("Version","2007-11-07"));
			urlVariablesArr.push(new SimpleDBAttributeObject("Action",action));
			urlVariablesArr.push(new SimpleDBAttributeObject("AWSAccessKeyId",_awsAccessKey));
			urlVariablesArr.push(new SimpleDBAttributeObject("Timestamp",generateRequestTimeStamp(new Date())));
			
			var urlVariables:URLVariables = generateSignature(urlVariablesArr, signatureVersionToUse,requestMethod);
			//use for sig 0			
			for each(var item:SimpleDBAttributeObject in urlVariablesArr)
				urlVariables[item.getAttributeName()] = item.getAttributeValue();

			var request:URLRequest = new URLRequest(remoteRequestURL);
				request.data = urlVariables;
				request.method = requestMethod;
			
			var urlLoader:URLLoader = new URLLoader();
				urlLoader.addEventListener(Event.COMPLETE,handleSimpleDBRequest);
				urlLoader.addEventListener(IOErrorEvent.IO_ERROR,handleSimpleDBRequestIOError);
				
				urlLoader.load(request);
				
				
				
				
				
			function handleSimpleDBRequest(event:Event):void
			{
				var simpleDBEvent:SimpleDBEvent = null;
				
				switch(urlVariables.Action)
				{
					case ACTION_CREATE_DOMAIN:
					
						simpleDBEvent = new SimpleDBEvent(SimpleDBEvent.DOMAIN_CREATED);
						break;
						
					case ACTION_DELETE_DOMAIN:
						simpleDBEvent = new SimpleDBEvent(SimpleDBEvent.DOMAIN_DELETED);
						break;
						
					case ACTION_LIST_DOMAINS:
						simpleDBEvent = new SimpleDBEvent(SimpleDBEvent.DOMAINS_LISTED);
						break;
						
					case ACTION_PUT_ATTRIBUTES:
						simpleDBEvent = new SimpleDBEvent(SimpleDBEvent.ATTRIBUTES_PUT);
						break;
						
					case ACTION_GET_ATTRIBUTES:
						simpleDBEvent = new SimpleDBEvent(SimpleDBEvent.ATTRIBUTES_RECEIVED);
						break;
						
					case ACTION_DELETE_ATTRIBUTES:
						simpleDBEvent = new SimpleDBEvent(SimpleDBEvent.ATTRIBUTES_REMOVED);
						break;
						
					case ACTION_QUERY:
						simpleDBEvent = new SimpleDBEvent(SimpleDBEvent.QUERY_COMPLETED);
						break;
						
					case ACTION_QUERY_WITH_ATTRIBUTES:
						simpleDBEvent = new SimpleDBEvent(SimpleDBEvent.QUERY_WITH_ATTRIBUTES_COMPLETED);
						break;
					case ACTION_GET_DOMAIN_METADATA:
						simpleDBEvent = new SimpleDBEvent(SimpleDBEvent.DOMAIN_METADATA_RECEIVED);
						break;
					case ACTION_QUERY_WITH_SQL_SYNTAX:
						simpleDBEvent = new SimpleDBEvent(SimpleDBEvent.QUERY_WITH_SQL_EXPRESSION_COMPLETED);
						break;					
						
				}


				
				if(simpleDBEvent != null && event.currentTarget.data != null)
				{
					if(event.currentTarget.data != null)
						simpleDBEvent.rawAmazonResponse = new XML(event.currentTarget.data);
						
					dispatchEvent(simpleDBEvent);
				}
				else
					dispatchEvent(new SimpleDBEvent(SimpleDBEvent.AWS_SIMPLEDB_ERROR));
			}	
			
			
			
			
			
			function handleSimpleDBRequestIOError(event:IOErrorEvent):void
			{
				trace(event);			
				var simpleDBEvent:SimpleDBEvent = new SimpleDBEvent(SimpleDBEvent.AWS_SIMPLEDB_ERROR);
				if(event.currentTarget.data != null)
					simpleDBEvent.rawAmazonResponse = new XML(event.currentTarget.data);
				
				simpleDBEvent.errorText = "an io error occured " + event.text;
					
				dispatchEvent(simpleDBEvent);
			}						
		}
		
		
		/*********************************************************************/
		
		private function generateRequestTimeStamp(d:Date):String
		{
			var splitArr:Array = null;
			var splitArr2:Array = null;
			
			var dd:Date = new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds());
			var ds:String = dateFormatter.format(dd);
			
			//AmazonSimpleDB requires a date formated according to the ISO8601 standard but
			//the dateFormatter class at this time doesn't return a two didget representation
			//of the hours value if it's less than 10.  BUMMER!!!
			if(d.getUTCHours() < 10)
			{
				splitArr = ds.split("T");
				//even worse they might fix this someday so before just
				//assuming a 0 can be prepended, first let's see if the
				//hour value is indeed only one character.

				var timeSplit:Array = splitArr[1].split(":");
				if(timeSplit[0].length < 2)
					ds = splitArr[0] + "T0" + splitArr[1];
			}
			
			//same with the days value
			if(d.getUTCDate() < 10)
			{
				splitArr = ds.split("T");
				
				splitArr2 = splitArr[0].split("-");
				if(splitArr2[2].length < 2)
					ds = splitArr2[0] + "-" + splitArr2[1] + "-0" + splitArr2[2] + "T" +  splitArr[1];
			}
			//and the month value
			if(d.getMonth() < 10)
			{
				splitArr = ds.split("T");
				
				splitArr2 = splitArr[0].split("-");
				if(splitArr2[1].length < 2)
					ds = splitArr2[0] + "-0" + splitArr2[1] + "-" + splitArr2[2] + "T" +  splitArr[1];				
			}
			
			
			//trace(ds);
			return ds;		
		}
		
		/*********************************************************************/
	    /**
	      * Computes RFC 2104-compliant HMAC signature for request parameters
	      * Implements AWS Signature, as per following spec:
	      *
	      * If Signature Version is 0, it signs concatenated Action and Timestamp
	      *.
	      */
		private function generateSignature(urlVariablesArr:Array, sigVersion:int = SIGNATURE_VERSION_0,requestMethod:String = "POST"):URLVariables
		{	
			var requestData:ByteArray = new ByteArray();
			var secretKeyAsBytes:ByteArray = new ByteArray();
			var urlVariables:URLVariables = new URLVariables();

			var hmacEncrypter:HMAC = null;
			switch (sigVersion)
			{

				case 0:	
					hmacEncrypter = new HMAC(new SHA1());

					var actionAttrib:SimpleDBAttributeObject = getAttributeObject("Action",urlVariablesArr);
					var timeStampObj:SimpleDBAttributeObject = getAttributeObject("Timestamp",urlVariablesArr);
					
					if(actionAttrib != null && timeStampObj != null)
						requestData.writeUTFBytes(actionAttrib.getAttributeValue() + timeStampObj.getAttributeValue());
					break;

				
				case 1: //deprecated
					
					break;
					
				case 2:
					hmacEncrypter = new HMAC(new SHA1());
					
					var strToSign:String = "";
					var accessKeyStr:String = "";
					urlVariablesArr.push(new SimpleDBAttributeObject("SignatureVersion",sigVersion.toString()));
					urlVariablesArr.push(new SimpleDBAttributeObject("SignatureMethod","HmacSHA1"));
				
					urlVariablesArr.sortOn("attribName",Array.CASEINSENSITIVE);
					
					for(var i:int = 0; i < urlVariablesArr.length; i++)
					{
						urlVariables[urlVariablesArr[i].getAttributeName()] = urlVariablesArr[i].getAttributeValue();
						
											
						if(urlVariablesArr[i].getAttributeName() == "AWSAccessKeyId")
							accessKeyStr = urlVariablesArr[i].getAttributeName() + "=" + urlVariablesArr[i].getAttributeValue();
						else
							strToSign += "&" + urlVariablesArr[i].getAttributeName() + "=" + encodeURIComponent(urlVariablesArr[i].getAttributeValue());
					}

											
					strToSign = requestMethod + "\n" + domainEndpoint + "\n" + endPointURLExtendsion + "\n" + accessKeyStr + strToSign;

					//encodeURIComponent() doesn't encode the ! character which is used in != queries
					while(strToSign.indexOf("!") >= 0)
						strToSign = strToSign.replace("!","%21");
			
					while(strToSign.indexOf("'") >= 0)
						strToSign = strToSign.replace("'","%27");
						
					while(strToSign.indexOf("*") > 0)
						strToSign = strToSign.replace("*","%2A");
						
					while(strToSign.indexOf("(") > 0)
						strToSign = strToSign.replace("(","%28");
						
					while(strToSign.indexOf(")") > 0)
						strToSign = strToSign.replace(")","%29");	
						
										
					
					//trace(strToSign);
					requestData.writeUTFBytes(strToSign);
				
					break;
			}
			secretKeyAsBytes.writeUTFBytes(_awsSecretKey);

			urlVariables.Signature = Base64.encodeByteArray(hmacEncrypter.compute(secretKeyAsBytes, requestData));
			//trace(urlVariables.Signature);
			return urlVariables;
		}	
		
		/*********************************************************************/
		
		private function getAttributeObject(key:String,attribsArray:Array):SimpleDBAttributeObject
		{
			if(attribsArray == null)
				return null;
				
			for each(var item:Object in attribsArray)
			{
				if(item is SimpleDBAttributeObject)
				{
					var attribObj:SimpleDBAttributeObject = item as SimpleDBAttributeObject;
					if(attribObj.getAttributeName() == key)
						return attribObj;
				}
			}
			return null;
		}			
	}//end class
}